﻿using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Serialization;

using SharpGL.Media3D;
using SharpGL.OpenGLAttributes;
using SharpGL.SceneGraph.Assets;
using SharpGL.SceneGraph.Helpers;

namespace SharpGL.SceneGraph.Primitives
{
    /// <summary>
    /// TexturedQuad is a four-point polygon filled with a bitmap of arbitrary size
    /// </summary>
    [DataContract(IsReference = true)]
    public class TexturedQuad : Polygon, IOwnOpenGLAttributesApply
    {
        #region Private Fields

        //Quad vectors.
        [DataMember()]
        private Point3D a = new Point3D(0.0f, 1.0f, 0.0f);
        [DataMember()]
        private Point3D b = new Point3D(0.0f, 0.0f, 0.0f);
        [DataMember()]
        private Point3D c = new Point3D(0.0f, 1.0f, 0.0f);
        [DataMember()]
        private Point3D d = new Point3D(1.0f, 1.0f, 0.0f);

        /// <summary>
        /// cellImage size
        /// </summary>
        [DataMember()]
        private Size cellSize = new Size(0, 0);

        /// <summary>
        /// This flag determines whether we need to build texture mipmaps.
        /// </summary>
        [DataMember()]
        private bool useMipmaps = false;

        /// <summary>
        /// The bounding volume helper - used to ease implementation of IBoundVolume.
        /// </summary>
        private BoundingVolumeHelper boundingVolumeHelper = new BoundingVolumeHelper();

        #endregion Private Fields

        #region Protected fubctions

        protected override void OnCreateInContext(OpenGL gl)
        {
            if (Material != null && Material.Texture != null && Material.Texture.Image != null)
            {
                Create(gl, a, b, c, d, Material.Texture.Image, cellSize);
                Validate(true, 0.0f);
            }
        }

        protected override void OnDestroyInContext(OpenGL gl)
        {
            base.OnDestroyInContext(gl);

            UVs.Clear();
            Normals.Clear();
            Vertices.Clear();
            Faces.Clear();
        }

        protected void AdjustCellSize(OpenGL gl, ref Size cellSize)
        {
            //  Get the max texture size.
            int[] textureMaxSize = { 0 };

            if (OpenGL.IsValidContext(gl))
                gl.GetInteger(OpenGL.GL_MAX_TEXTURE_SIZE, textureMaxSize);
            else
                textureMaxSize[0] = 1;

            //  If cell size parameter is not a power of two, set it as a nearest lower power of two.
            if (cellSize.Height == 0)
                cellSize.Height = textureMaxSize[0];
            else
                cellSize.Height = MathHelper.RoundDownToPowerOf2(cellSize.Height);

            if (cellSize.Width == 0)
                cellSize.Width = textureMaxSize[0];
            else
                cellSize.Width = MathHelper.RoundDownToPowerOf2(cellSize.Width);
        }
        
        /// <summary>
        /// Computes a length of cell side in 3d space.
        /// </summary>
        /// <param name="vBegin">Start point.</param>
        /// <param name="vEnd">End point.</param>
        /// <param name="fLenCoeff">Length coefficient.</param>
        /// <param name="uiStepsCount">Number of steps.</param>
        /// <returns></returns>
        protected Vector3D ComputeSteps(Point3D vBegin, Point3D vEnd, float fLenCoeff, int uiStepsCount)
        {
            if (uiStepsCount != 0)
            {
                return (vEnd - vBegin) * fLenCoeff / uiStepsCount;
            }
            else
            {
                return new Vector3D(0.0f, 0.0f, 0.0f);
            }
        }

        protected Bitmap CropBitmap(Bitmap bitmap, Rectangle rect)
        {
            Bitmap dest = new Bitmap(rect.Width, rect.Height);
            Rectangle destRect = new Rectangle(0, 0, rect.Width, rect.Height);
            using (Graphics graphics = Graphics.FromImage(dest))
            {
                graphics.DrawImage(bitmap, destRect, rect, GraphicsUnit.Pixel);
            }
            return dest;

            /*const int BPP = 4;        //4 Bpp = 32 bits, 3 = 24, etc.
            
            BitmapData originalRaw = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

            int originalByteCount = originalRaw.Stride * originalRaw.Height;
            byte[] originalBytes = new Byte[originalByteCount];
            Marshal.Copy(originalRaw.Scan0, originalBytes, 0, originalByteCount);

            Bitmap croppedBitmap = new Bitmap(rect.Width, rect.Height);
            BitmapData croppedRaw = croppedBitmap.LockBits(new Rectangle(0, 0, rect.Width, rect.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
            int croppedByteCount = croppedRaw.Stride * croppedRaw.Height;
            byte[] croppedBytes = new Byte[croppedByteCount];

            //Iterate the selected area of the original image, and the full area of the new image
            for (int i = 0; i < rect.Height; i++)
            {
                for (int j = 0; j < rect.Width * BPP; j += BPP)
                {
                    int originalIndex = (rect.Y * originalRaw.Stride) + (i * originalRaw.Stride) + (rect.X * BPP) + (j);
                    int croppedIndex = (i * croppedRaw.Stride) + (j);

                    //copy data: once for each channel
                    for (int k = 0; k < BPP; k++)
                    {
                        croppedBytes[croppedIndex + k] = originalBytes[originalIndex + k];
                    }
                }
            }

            //copy new data into a bitmap
            Marshal.Copy(croppedBytes, 0, croppedRaw.Scan0, croppedByteCount);

            bitmap.UnlockBits(originalRaw);
            croppedBitmap.UnlockBits(croppedRaw);

            return croppedBitmap;*/
        }

        /// <summary>
        /// This function divides bitmap to array of cells of defined size and creates a texture from each cell.
        /// Then it calculate polygon's geometry according to cell information, creating a face for each cell.
        /// </summary>
        /// <param name="gl">Current OpenGL instance</param>
        /// <param name="useMipmaps">Flag to create a mipmapped textures</param>
        /// <returns></returns>
        protected bool Create(OpenGL gl, Point3D a, Point3D b, Point3D c, Point3D d, Bitmap image, Size cellSize)
        {
            //  Check parameters.
            if (!OpenGL.IsValidContext(gl) || image == null)
                return false;

            Size fullSize = image.Size;

            AdjustCellSize(gl, ref cellSize);

            //  Calculate amount of rows and columns of cells
            int cellRows = (int)Math.Ceiling((float)image.Height / cellSize.Height);
            int cellCols = (int)Math.Ceiling((float)image.Width / cellSize.Width);

            //  Now we have full size, cell size, amount of rows and cols
            //  can create geometry.
            
            //  Fill UV list.
            this.UVs.Clear();
            this.UVs.Add(new UV(0.0f, 0.0f)); //  0
            this.UVs.Add(new UV(1.0f, 0.0f)); //  1
            this.UVs.Add(new UV(0.0f, 1.0f)); //  2
            this.UVs.Add(new UV(1.0f, 1.0f)); //  3

            //  Fill vectors list.
            FillVerticesList(fullSize, cellSize, a, b, c, d, this.Vertices);
            
            //  As soon as we have vectors, then creating faces.
            bool isExistReducedCells = (cellSize.Width * cellCols != fullSize.Width) || (cellSize.Height * cellRows != fullSize.Height);
            Faces.Clear();

            for (int row = 0; row < cellRows; row++)
            {
                for (int column = 0; column < cellCols; column++)
                {
                    Size currentCellSize = cellSize;

                    bool isLastCol = (column == (cellCols - 1));
                    bool isLastRow = (row == (cellRows - 1));

                    //  If current cell is a last row or column and it is cell of reduced size, calculate its size.
                    if (isLastCol && isExistReducedCells)
                    {
                        currentCellSize.Width = fullSize.Width - (column * cellSize.Width);
                    }
                    if (isLastRow && isExistReducedCells)
                    {
                        currentCellSize.Height = fullSize.Height - (row * cellSize.Height);
                    }

                    //  Cut a cell from image.
                    //Bitmap cellImage = image.Clone(new Rectangle(column * cellSize.Width, row * cellSize.Height, currentCellSize.Width, currentCellSize.Height), PixelFormat.Format32bppArgb);
                    Bitmap cellBitmap = CropBitmap(image, new Rectangle(column * cellSize.Width, row * cellSize.Height, currentCellSize.Width, currentCellSize.Height));

                    //  Calculate point indices.
                    int topIndex = row * (cellCols + 1) + column;
                    int bottomIndex = (row + 1) * (cellCols + 1) + column;

                    //  Creating face.
                    Face newFace = new Face();
                    newFace.Indices.Add(new Index(topIndex, 0));
                    newFace.Indices.Add(new Index(bottomIndex, 2));
                    newFace.Indices.Add(new Index(bottomIndex + 1, 3));
                    newFace.Indices.Add(new Index(topIndex + 1, 1));
                    newFace.Material = new Material();
                    newFace.Material.Texture = new Texture2D();
                    newFace.Material.Texture.UseMipmaps = useMipmaps;
                    newFace.Material.Texture.Create(gl, cellBitmap);
                    Faces.Add(newFace);
                }
            }

            //  All done!
            return true;
        }

        /// <summary>
        /// This function divides the original quad into a cells according to divided texture.
        /// An array of vectors is filled with vectors of cells.
        /// </summary>
        /// <param name="fullSize">The full image size.</param>
        /// <param name="cellSize">The cell size.</param>
        /// <param name="a">Left top.</param>
        /// <param name="b">Left bottom.</param>
        /// <param name="c">Right bottom.</param>
        /// <param name="d">Right top.</param>
        /// <param name="vectors">Out vectors list for fill.</param>
        protected void FillVerticesList(Size fullSize, Size cellSize, Point3D a, Point3D b, Point3D c, Point3D d, IList<Point3D> vertices)
        {
            //  Calculate amount of rows and columns of cells
            int cellRows = (int)Math.Ceiling((float)fullSize.Height / cellSize.Height);
            int cellCols = (int)Math.Ceiling((float)fullSize.Width / cellSize.Width);

            //  Calculate a intermediate points between the polygon vectors.
            float fullHeightCoeff = (float)cellRows * cellSize.Height / fullSize.Height;
            float fullWidthCoeff = (float)cellCols * cellSize.Width / fullSize.Width;
            
            //  Calculate steps.
            Vector3D stepAB = ComputeSteps(a, b, fullHeightCoeff, cellRows);
            Vector3D stepDC = ComputeSteps(d, c, fullHeightCoeff, cellRows);

            //  Clear vectors list.
            vertices.Clear();

            //  Fill vectors list.
            for (int row = 0; row <= cellRows; row++)
            {
                bool isLastRow = (row == cellRows);

                Point3D A1 = isLastRow ? b : a + (stepAB * row);
                Point3D D1 = isLastRow ? c : d + (stepDC * row);

                Vector3D stepA1D1 = ComputeSteps(A1, D1, fullWidthCoeff, cellCols);

                for (int col = 0; col <= cellCols; col++)
                {
                    bool isLastColumn = (col == cellCols);

                    Point3D gridPoint = isLastColumn ? D1 : A1 + (stepA1D1 * col);

                    vertices.Add(gridPoint);
                }
            }
        }

        #endregion Protected fubctions

        #region Constructors

        public TexturedQuad()
        {
            Name = "TexturedQuad";
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="a">Left top.</param>
        /// <param name="b">Left bottom.</param>
        /// <param name="c">Right bottom.</param>
        /// <param name="d">Right top.</param>
        /// <param name="cellsize">cellImage size.</param>
        public TexturedQuad(Point3D a, Point3D b, Point3D c, Point3D d, Size cellSize)
            : this()
        {
            this.a = a;
            this.b = b;
            this.c = c;
            this.d = d;
            this.cellSize = cellSize;
        }

        #endregion Constructors

        #region Public Methods
        /*
        /// <summary>
        /// Uses OpenGl attributes on own way.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        /// <param name="attributesGroup">The OpenGL attributes group.</param>
        public virtual void ApplyAttributes(OpenGL gl, OpenGLAttributesGroup attributesGroup)
        {
            if (attributesGroup != null && attributesGroup.TextureAttributes.AreAnyAttributesSet())
            {
                foreach (Face face in Faces)
                {
                    if (face.Material != null && face.Material.Texture != null)
                    {
                        face.Material.Texture.Bind(gl);
                        attributesGroup.TextureAttributes.SetAttributes(gl);
                    }
                }
            }
        }
        */
        #endregion Public Methods

        #region Public Properties

        public Point3D A
        {
            get { return a; }
            set
            {
                a = value;

                if (OpenGL.IsValidContext(CurrentOpenGLContext) &&
                    Material != null && Material.Texture != null && Material.Texture.Image != null)
                {
                    Size cellSize = this.cellSize;
                    AdjustCellSize(CurrentOpenGLContext, ref cellSize);
                    FillVerticesList(Material.Texture.Image.Size, cellSize, a, b, c, d, Vertices);
                }

                NotifyPropertyChanged("A");
            }
        }

        public Point3D B
        {
            get { return b; }
            set
            {
                b = value;

                if (OpenGL.IsValidContext(CurrentOpenGLContext) &&
                    Material != null && Material.Texture != null && Material.Texture.Image != null)
                {
                    Size cellSize = this.cellSize;
                    AdjustCellSize(CurrentOpenGLContext, ref cellSize);
                    FillVerticesList(Material.Texture.Image.Size, cellSize, a, b, c, d, Vertices);
                }

                NotifyPropertyChanged("B");
            }
        }

        public Point3D C
        {
            get { return c; }
            set
            {
                c = value;

                if (OpenGL.IsValidContext(CurrentOpenGLContext) &&
                    Material != null && Material.Texture != null && Material.Texture.Image != null)
                {
                    Size cellSize = this.cellSize;
                    AdjustCellSize(CurrentOpenGLContext, ref cellSize);
                    FillVerticesList(Material.Texture.Image.Size, cellSize, a, b, c, d, Vertices);
                }

                NotifyPropertyChanged("C");
            }
        }

        public Point3D D
        {
            get { return d; }
            set
            {
                d = value;

                if (OpenGL.IsValidContext(CurrentOpenGLContext) &&
                    Material != null && Material.Texture != null && Material.Texture.Image != null)
                {
                    Size cellSize = this.cellSize;
                    AdjustCellSize(CurrentOpenGLContext, ref cellSize);
                    FillVerticesList(Material.Texture.Image.Size, cellSize, a, b, c, d, Vertices);
                }

                NotifyPropertyChanged("D");
            }
        }

        /// <summary>
        /// cellImage size.
        /// </summary>
        public Size CellSize
        {
            get { return cellSize; }
            set
            {
                cellSize = value;

                if (OpenGL.IsValidContext(CurrentOpenGLContext))
                {
                    DestroyInContext();
                    CreateInContext(CurrentOpenGLContext);
                }

                NotifyPropertyChanged("CellSize");
            }
        }

        /// <summary>
        /// If true, use mipmap textures.
        /// </summary>
        public bool UseMipmaps
        {
            get { return useMipmaps; }
            set
            {
                useMipmaps = value;

                if (OpenGL.IsValidContext(CurrentOpenGLContext))
                {
                    DestroyInContext();
                    CreateInContext(CurrentOpenGLContext);
                }

                NotifyPropertyChanged("UseMipmaps");
            }
        }

        #endregion Public Properties
    }
}
